package fr.upmc.aladyn.shared;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * Object class with an unique instance. Contains methods tied to the 
 * <code> Transactionable </code> annotation like saving/restoring 
 * Transactionable objects and determining if we are in a transaction phase 
 * or not.
 * 
 * @author Thierry Wong
 * @author Seyyid ahmed Ouir 
 */

public class ObjectSingleton 
{
	private boolean trans;
	private ArrayList<HashMap<Object, Object>> environnements;
	private ArrayList<Integer> indexList;
	private static ObjectSingleton instance = null;
	
	/**
	 * Default constructor.
	 */
	public ObjectSingleton() 
	{
		trans = false;
		environnements = new ArrayList<>();
		indexList = new ArrayList<>();
	}
	
	/**
	 * If an instance of <code> ObjectSingleton </code> has already been 
	 * initialized, this method return this instance of the object. Otherwise, 
	 * it returns a new instance of <code> ObjectSingleton </code>.
	 * 
	 * @return Type <code>ObjectSingleton</code> represents actual instance.
	 */
	public static ObjectSingleton getInstance() 
	{
		if(instance == null)
		{
			instance = new ObjectSingleton();
		}
		return instance;
	}
	
	public ArrayList<HashMap<Object, Object>> getEnvironnements() {
		return environnements;
	}

	public ArrayList<Integer> getIndexList() {
		return indexList;
	}
	
	private int getIndexCourant() {
		return indexList.get(indexList.size()-1);
	}
	
	/**
	 * Returns whether we are in a transaction phase or not. TRUE if a we are in
	 * a <code> Transactionable </code> method. FALSE Otherwise.
	 * 
	 * @return Type <code>boolean</code> represents transaction status.
	 */
	public boolean transactionEnCours() 
	{
		return trans;
	}

	/**
	 * Method to execute at the beginning of a <code> Transactionable </code> 
	 * method. It will initialize the environment to allow us to save and 
	 * restore <code> Transactionable </code> objects met during execution of 
	 * the method. 
	 */
	public void debutMethodeTrans() 
	{
		environnements.add(new HashMap<>());
		indexList.add(environnements.size()-1);
		trans = true;
	}

	/** 
	 * Method to execute at the end of a <code> Transactionable </code> method.
	 * It will tear down allocated structures used to save and restore 
	 * <code> Transactionable </code> objects. Previously saved objects will
	 * not be accessible anymore.
	 */
	public void finMethodeTrans() 
	{
		indexList.remove(indexList.size()-1);
		if(indexList.size() == 0)
		{
			environnements.clear();
			trans = false;
		}
	}

	/**
	 * Saves the <code> Object </code> passed as parameter by adding it to the 
	 * environment corresponding to our current <code> Transactionable </code> 
	 * method. 
	 * 
	 * @param objetAsauvegarder Type <code> Object </code> we wish to save.
	 */
	public void sauvegarder(Object objetAsauvegarder) 
	{

		if (trans) 
		{
			HashMap<Object, Object> envCourant = 
					environnements.get(getIndexCourant());
			for(int i = getIndexCourant(); i < environnements.size(); i++) 
			{
				if(environnements.get(i).containsKey(objetAsauvegarder)) 
				{
					return;
				}
			}
			sauvegarderObjet(objetAsauvegarder, envCourant);
		}
	}

	/**
	 * Internal method used by method <code> sauvegarder </code>. Does the 
	 * actual saving.
	 * 
	 * @param objetAsauvegarder Type <code>Object</code> we wish to save.
	 * @param environnement Contains all Transactionable objects saved.
	 */
	private void sauvegarderObjet(Object objetAsauvegarder, 
			HashMap<Object, Object> environnement)
	{
		Object copie = null;
		Class<?> c = objetAsauvegarder.getClass();
		try {
			Constructor tCons = null;
			// Test to see if we have a null constructor
			for (Constructor cons : c.getConstructors())
			{
				tCons = cons;
				if (cons.getParameterTypes().length == 0)
				{
					// If class contains a null constructor, we can exit
					break;
				}
			}
			
			if (tCons.getParameterTypes().length == 0)
			{
				// Current class contains a default constructor, we can use the
				// Class.newInstance() method to create a copy.
				copie = c.newInstance();				
			}
			else{
				// Current class does not have a null constructor, in this case,
				// we have to use the Constructor.newInstance() method. 
				// We need to retrieve the parameter types and determine 
				// what values to give to our newInstance().
				// At the moment, throws a InstanciationException since we do 
				// not give it any parameters.
				copie = tCons.newInstance();
			}
			
			// We save the object's current fields as well as inherited fields.
			while (c != Object.class){
				for (Field f : c.getDeclaredFields())
				{
					if (f.isAccessible())
					{
						f.set(copie, f.get(objetAsauvegarder));
					}
					else
					{
						f.setAccessible(true);
						f.set(copie, f.get(objetAsauvegarder));
						f.setAccessible(false);
					}
				}
				c = c.getSuperclass();
			}
		} 
		catch (InstantiationException | 
				IllegalAccessException | 
				IllegalArgumentException | 
				InvocationTargetException e2) 
		{
			e2.printStackTrace();
		}
		// We add the copy to our environment
		environnement.put(objetAsauvegarder, copie);
	}

	/**
	 * Method to execute in the try/catch encapsuling the entire 
	 * <code> Transactionable </code> method. If an exception is thrown, it
	 * restores the saved objects and then cleans up the environment.
	 * 
	 */
	public void restaurerCatchFinal() 
	{
		restaurer();
		environnements.remove(getIndexCourant());
		finMethodeTrans();
	}

	/**
	 * Method to execute to restore a <code> Transactionable </code> object. 
	 */
	public void restaurer() 
	{
		int indexDernierEnv = environnements.size()-1;
		
		for (int i = indexDernierEnv; i > getIndexCourant(); i--) 
		{
			restaurerEnvironnement(environnements.get(i));
			environnements.remove(i);
		}
		restaurerEnvironnement(environnements.get(getIndexCourant()));
	}

	/**
	 * Internal method called by <code> restaurer </code>. Retrieves the 
	 * environment entry corresponding to the Object to restore and launches
	 * it's restoration
	 * 
	 * @param environnement Environment from which we restore the Objects.
	 */
	private void restaurerEnvironnement(HashMap<Object, Object> environnement) 
	{
		Object objetArestaurer, copie;

		for (Map.Entry<Object, Object> entry : environnement.entrySet()) 
		{
			objetArestaurer = entry.getKey();
			copie = entry.getValue();
			restaurerObjet(objetArestaurer, copie);
		}
		
		environnement.clear();
	}

	/**
	 * Restores the Object passed as first parameter to it's state in the second
	 * parameter.
	 * 
	 * @param objetArestaurer Object we want to restore.
	 * @param copie Stored version of the Object we want to go back to
	 */
	private void restaurerObjet(Object objetArestaurer, Object copie) 
	{
		Class<?> c = objetArestaurer.getClass();
		try 
		{
			while (c != Object.class){
				for (Field f : c.getDeclaredFields())
				{
					if (f.isAccessible())
					{
						f.set(objetArestaurer, f.get(copie));
					}
					else
					{
						f.setAccessible(true);
						f.set(objetArestaurer, f.get(copie));
						f.setAccessible(false);
					}
				}
				c = c.getSuperclass();
			}
		} 
		catch (IllegalArgumentException | IllegalAccessException e1) 
		{
			e1.printStackTrace();
		}	
	}
}
